﻿// Copyright (C) 2013-2014 Ralph Becket
// Released under a variant of the Microsoft Public License -- see ../LICENCE.txt.
//
// Rawb: a collection of Knockout UI Web controls
// by Ralph Becket.  Written because what I've
// found on the web is either woefully bad or
// dictates too much policy to the user.
//
// If the documentation syntax looks a little odd, that's because
// I've employed the CodePlex wiki style.

var rawb = rawb || {};

(function () {

    // The identity function.  This comes in helpful all over the place.

    rawb.id = function (x) { return x; };

    // A utility function to ensure a quantity is represented as an observable.

    var ensureObs = function (x) { return ko.isObservable(x) ? x : ko.observable(x); };

    /*

!! rawb.ScrollBar(range, start, count, hardStop, isHorizontal)
*Templates:* _RawbVScrollBarTemplate, RawbHScrollBarTemplate_

A styleable scrollbar managing an integer observable spanning a given range of
values.  The value can be changed by the user dragging the scrollbar
thumb with the mouse.  The displayed scrollbar position can be changed by the
program by changing the start, count, and range observables.

See _rawb.VScrollBar_ and _rawb.HScrollBar_.

*Parameters*

*range:* _number_
Required; may be observable; exposed as an observable.
The scrollbar ranges over 0..(range - 1).

*start:* _number_
Required; may be observable; exposed as an observable.
The point in the range where the scrollbar thumb starts.

*count:* _number_
Required; may be observable; exposed as an observable.
The number of points over which the scrollbar thumb extends.
The display of the scrollbar thumb has a lower-limit of 10% of the range,
to prevent the thumb from apparently disappearing when the range is very large.

*hardStop:* _boolean_
Optional (default _false_); must not be an observable; not exposed.
If _true_, constrains _start + count <= range_.
If _false_, constrains _start < range_.

*isHorizontal:* _boolean_
Optional (default _false_); must not be an observable; not exposed.
If _true_, scrollbar thumb responds to horizontal mouse dragging.
If _false_, scrollbar thumb responds to vertical mouse dragging.

*Methods*

*dispose(): _function ()_
This function clears up all subscriptions etc. created by the grid.
Use this when the _Grid_ instance goes out of scope, if you are concerned about potential memory leaks.

    */
    var ScrollBar = function (range, start, count, hardStop, isHorizontal) {
        var self = this;
        // if (!ko.observable(start)) throw "rawb.ScrollBar: range must be  XXX HERE!
        self.range = ensureObs(range);
        self.start = ensureObs(start);
        self.count = ensureObs(count);
        self.hardStop = (hardStop ? 1 : 0);
        // The start of the thumb as a percentage of the range.
        self.startPc = ko.computed(function () {
            return range() === 0 ? 0 : 100 * start() / range();
        });
        // The size of the thumb as a percentage of the range,
        // with a minimum of 10%.
        self.sizePc = ko.computed(function () {
            return range() === 0 ? 100 : Math.max(10, 100 * count() / range());
        });
        self.thumbSize = ko.computed(function () {
            return range() === 0 ? 100 : 100 * count() / range();
        });
        // When the user starts dragging the thumb, we pass control to the root window
        // so the user can drag the mouse anywhere on the screen before releasing the
        // mouse button.
        self.startDragging = function (data, evt) {
            if (self.range() === 0) return;
            var thumb = evt.target;
            var track = thumb.parentNode;
            var trackPx = parseInt(isHorizontal ? track.style.width : track.style.height);
            self.pxPerUnit = trackPx / self.range();
            self.xy0 = (isHorizontal ? evt.screenX : evt.screenY);
            self.start0 = self.start();
            self.windowonmousemove = window.onmousemove;
            self.windowonmouseup = window.onmouseup;
            window.onmousemove = self.drag;
            window.onmouseup = self.stopDragging;
        };
        // The thumb start is adjusted at each mouse move event during dragging.
        self.drag = function (evt) {
            var xy = (isHorizontal ? evt.screenX : evt.screenY);
            var pxDelta = xy - self.xy0;
            var unitDelta = pxDelta / self.pxPerUnit;
            //self.start(Math.max(0, Math.min(self.range() - self.hardStop * self.count() - 1, Math.floor(self.start0 + unitDelta))));
            self.pendingStart = Math.max(0, Math.min(self.range() - self.hardStop * self.count() - 1, Math.floor(self.start0 + unitDelta)));
            if (!self.pendingStartUpdateTask) {
                self.pendingStartUpdateTask = setTimeout(self.pendingStartUpdate, 20); // Cap updates at 50 per second.
            }
        };
        self.pendingStartUpdate = function () {
            self.start(self.pendingStart);
            self.pendingStartUpdateTask = undefined;
        };
        // When the mouse button is released, the root window event handlers are
        // restored.
        self.stopDragging = function () {
            window.onmousemove = self.windowonmousemove;
            window.onmouseup = self.windowonmouseup;
            self.windowonmousemove = undefined;
            self.windowonmouseup = undefined;
        };
        self.dispose = function () { };

        return self;
    };

    /*

!! rawb.VScrollBar(range, start, count, hardStop)
*Templates:* _RawbVScrollBarTemplate_

Shorthand for _rawb.ScrollBar(range, start, count, hardStop, false)_.

*Example HTML:*
{code:html}
<!-- ko template: { name: 'RawbVScrollBarTemplate', data: vsb } -->
<!-- /ko -->
{code:html}

*Example JavaScript:*
{code:javascript}
var range = ko.observable(100);
var start = ko.observable(0);
var count = ko.observable(30);
var hardStop = true;
var vsb = new rawb.VScrollBar(range, start, count, hardStop);
ko.applyBindings({vsb: vsb});
{code:javascript}

    */
    rawb.VScrollBar = function (range, start, count, hardStop) {
        return ScrollBar.call(this, range, start, count, hardStop, false);
    }

    /*

!! rawb.HScrollBar(range, start, count, hardStop)
*Templates:* _RawbHScrollBarTemplate_

Shorthand for _rawb.ScrollBar(range, start, count, hardStop, true)_.

*Example HTML:*
{code:html}
<!-- ko template: { name: 'RawbHScrollBarTemplate', data: hsb } -->
<!-- /ko -->
{code:html}

*Example JavaScript:*
{code:javascript}
var range = ko.observable(100);
var start = ko.observable(0);
var count = ko.observable(30);
var hardStop = true;
var hsb = new rawb.HScrollBar(range, start, count, hardStop);
ko.applyBindings({hsb: hsb});
{code:javascript}

    */
    rawb.HScrollBar = function (range, start, count, hardStop) {
        return ScrollBar.call(this, range, start, count, hardStop, true);
    }

    /*

!! rawb.NativeVScrollBar({height, contentHeight, scrollTop, top, left, width})
*Templates:* _RawbNativeVScrollBarTemplate_

A browser-native vertical scrollbar which can be used to control another observable (*scrollTop*)
either from the program or via user interaction with the scrollbar control.  No 'content' is
shown, just the scrollbar itself.

*Example HTML:*
{code:html}
<!-- ko template: { name: 'RawbNativeVScrollBarTemplate', data: vsb, afterRender: vsb.afterRender } -->
<!-- /ko -->
{code:html}

*Example JavaScript:*
{code:javascript}
var height = ko.observable(100);
var contentHeight = ko.observable(200);
var scrollTop = ko.observable(30);
var vsb = new rawb.NativeVScrollBar(height, contentHeight, scrollTop);
ko.applyBindings({vsb: vsb});
{code:javascript}

*Parameters*

*height:* _number_
Optional (default _200_); may be observable; exposed as an observable.

The height of the scrollbar control in pixels.

*contentHeight:* _number_
Optional (default _400_); may be observable; exposed as an observable.

The height of the 'contents' of the scrollbar in pixels.

*scrollTop:* _number_
Optional (default _0_); may be observable; exposed as an observable.

The distance from the top of the 'contents' to the 'displayed' region in pixels.

*top:* _number_
Optional (default _0_); may be observable; exposed as an observable.

The top offset in pixels for the absolute position of the scrollbar.

*left:* _number_
Optional (default _0_); may be observable; exposed as an observable.

The left offset in pixels for the absolute position of the scrollbar.

*width:* _number_
Optional (default _17_); may be observable; exposed as an observable.

The width of the scrollbar control.

*Methods*

*dispose(): _function ()_
This function clears up all subscriptions etc. created by the scrollbar.
Use this when the _NativeVScrollBar_ instance goes out of scope, if you are concerned about potential memory leaks.

    */

    rawb.NativeVScrollBarDefaults = {
        height: 200,
        width: 17,
        top: 0,
        left: 0,
        contentHeight: 400,
        scrollTop: 100
    };

    // NOTE: the template construction *must* invoke .afterRender
    // or the control will not initalise properly.
    rawb.NativeVScrollBar = function (args) {
        var self = this;
        args = args || {};
        var getArg = function (name) {
            var x = args[name];
            return !x
                 ? ko.observable(rawb.NativeVScrollBarDefaults[name])
                 : !ko.isObservable(x)
                 ? ko.observable(x)
                 : x;
        };
        self.height = getArg("height");
        self.width = getArg("width");
        self.top = getArg("top");
        self.left = getArg("left");
        self.contentHeight = getArg("contentHeight");
        self.scrollTop = getArg("scrollTop");
        self.element = undefined;
        self.afterRender = function (elements) {
            self.element = elements.filter(function (elt) {
                var classList = elt.classList;
                return classList && classList.contains("rawb-native-v-scrollbar");
            })[0];
            if (!self.element) {
                throw "rawb.NativeVScrollBar: afterRender could not find the rawb-native-v-scrollbar";
            }
            self.element.scrollTop = self.scrollTop();
        };
        self.onScroll = function (data, evt) {
            self.scrollTop(self.element.scrollTop);
        };

        self.subscriptions = [
            self.scrollTop.subscribe(function (x) {
                self.element.scrollTop = x;
            })
        ];

        self.dispose = function () {
            while (self.subscriptions.length !== 0) {
                self.subscriptions.pop().dispose();
            }
        };

        return self;
    };


    // ---- "Draggers" ----
    //
    // Rawb draggers are typically used for "resize" bars along a single axis.
    // This is the 2D dragger, of which the VDragger and HDragger are special
    // cases.

    /*

!! rawb.Dragger(hObs, hLimitFn, hConsistencyPred, vObs, vLimitFn, vConsistencyPred, updateThrottle)
*Template:* _any_

Draggers are used to manage mouse dragging in either or both axes.
If _d_ is a _Dragger_, bind _d.startDragging_ to the _mousedown_ event of
the element requiring dragging behaviour.

See _rawb.HDragger_ and _rawb.VDragger_.

*Parameters*

*hObs:* _number_
Optional (default _undefined_); must be observable; exposed as an observable.

The observable number affected by horizontal mouse dragging (dragging to
the right by _n_ pixels increases the value of _hObs_ by _n_).

*hLimitFn:* _function (number) -> number_
Optional (default _identity_); must not be an observable; exposed as non-observable.

The _hLimitFn_ can be used to bound changes to _hObs_ by mouse dragging.

*hConsistencyPred:* _function () -> boolean_
Optional (default _truth_); must not be an observable; exposed as non-observable.

The _hConsistencyPred_ can be used to prevent changes to _hObs_ which would
cause some condition to be violated.  That is, if dragging changes _hObs_ in
such a way that _hConsistencyPred_ returns false, then _hObs_ is restored to its
immediately previous value.

*vObs:* _number_
Optional (default _undefined_); must be observable; exposed as an observable.

The observable number affected by vertical mouse dragging (dragging to
the right by _n_ pixels increases the value of vObs by _n_).

*vLimitFn:* _function (number) -> number_
Optional (default _identity_); must not be an observable; exposed as non-observable.

The _vLimitFn_ can be used to bound changes to _vObs_ by mouse dragging.

*vConsistencyPred:* _function () -> boolean_
Optional (default _truth_); must not be an observable; exposed as non-observable.

The _vConsistencyPred_ can be used to prevent changes to _vObs_ which would
cause some condition to be violated.  That is, if dragging changes _vObs_ in
such a way that _vConsistencyPred_ returns false, then _vObs_ is restored to its
immediately previous value.

*updateThrottle:* _number_
Optional (default _20_); must not be an observable; exposed as non-observable.

The minimum number of milliseconds between successive updates of _hObs_ or _vObs_.
Since changing these on every mouse move is usually undesirable, this throttle
allows some control over the rate of updates.

*Methods*

*dispose(): _function ()_
This function clears up all subscriptions etc. created by the grid.
Use this when the _Grid_ instance goes out of scope, if you are concerned about potential memory leaks.

    */
    rawb.Dragger = function (hObs, hLimitFn, hConsistencyPred, vObs, vLimitFn, vConsistencyPred, updateThrottle) {
        var self = this;
        self.hObs = hObs;
        self.hLimit = hLimitFn || function (x) { return x; };
        self.hConsistent = hConsistencyPred || function (x) { return true; };
        self.vObs = vObs;
        self.vLimit = vLimitFn || function (x) { return x; };
        self.vConsistent = vConsistencyPred || function (x) { return true; };
        self.updateThrottle = updateThrottle || 20;
        self.startDragging = function (data, evt) {
            self.x0 = evt.screenX;
            self.y0 = evt.screenY;
            self.hObs0 = self.hObs && self.hObs();
            self.vObs0 = self.vObs && self.vObs();
            self.windowonmousemove = window.onmousemove;
            self.windowonmouseup = window.onmouseup;
            window.onmousemove = self.drag;
            window.onmouseup = self.stopDragging;
        };
        self.drag = function (evt) {
            self.xDelta = evt.screenX - self.x0;
            self.yDelta = evt.screenY - self.y0;
            if (!self.pendingDragTask) self.pendingDragTask = setTimeout(self.handlePendingDrag, self.updateThrottle);
        };
        self.handlePendingDrag = function (evt) {
            if (self.hObs) {
                var oldHObs = self.hObs();
                var newHObs = self.hLimit(self.hObs0 + self.xDelta);
                self.hObs(newHObs);
                if (!self.hConsistent()) {
                    // This change made something inconsistent, so undo it.
                    self.hObs(oldHObs);
                }
            }
            if (self.vObs) {
                var oldVObs = self.vObs();
                var newVObs = self.vLimit(self.vObs0 + self.yDelta);
                self.vObs(newVObs);
                if (!self.vConsistent()) {
                    // This change made something inconsistent, so undo it.
                    self.vObs(oldVObs);
                }
            }
            self.pendingDragTask = undefined;
        }
        self.stopDragging = function () {
            window.onmousemove = self.windowonmousemove;
            window.onmouseup = self.windowonmouseup;
            self.windowonmousemove = undefined;
            self.windowonmouseup = undefined;
        };
        self.dispose = function () { };

        return self;
    };

    /*
    
!! rawb.HDragger(obs, limitFn, consistencyPred, updateThrottle)
*Template:* _any_

Shorthand for _rawb.Dragger(obs, limitFn, consistencyPred, undefined, undefined, undefined, updateThrottle)_.

*Example HTML:*
{code:html}
<div data-bind="event: { mousedown: hd.startDragging }">
</div>
{code:html}

*Example JavaScript:*
{code:javascript}
var x = ko.observable(100);
var limitFn = function (x) { return Math.max(0, Math.min(100, x)); };
var consistencyPred = undefined;
ko.applyBindings({hd: new rawb.HDragger(x, limitFn, consistencyPred)});
{code:javascript}

    */
    rawb.HDragger = function (obs, limitFn, consistencyPred) {
        return rawb.Dragger.call(this, obs, limitFn, consistencyPred, null, null, null);
    };

    /*

!! rawb.VDragger(obs, limitFn, consistencyPred, updateThrottle)
*Template:* _any_

Shorthand for _rawb.Dragger(undefined, undefined, undefined, obs, limitFn, consistencyPred, updateThrottle)_.

*Example HTML:*
{code:html}
<div data-bind="event: { mousedown: vd.startDragging }">
</div>
{code:html}

*Example JavaScript:*
{code:javascript}
var x = ko.observable(100);
var limitFn = function (x) { return Math.max(0, Math.min(100, x)); };
var consistencyPred = undefined;
ko.applyBindings({vd: new rawb.VDragger(x, limitFn, consistencyPred)});
{code:javascript}

    */
    rawb.VDragger = function (obs, limitFn, consistencyPred) {
        return rawb.Dragger.call(this, null, null, null, obs, limitFn, consistencyPred);
    };

    // ---- The grid ----

    rawb.GridDefaults = {
        data: [],
        width: 400,
        height: 200,
        vScrollBarWidth: 17,
        hScrollBarHeight: 17,
        resizerWidth: 2,
        headerRowHeight: 25,
        valueRowHeight: 25,
        footerRowHeight: 25,
        rowsPerStripe: 1,
        headerTemplateID: "RawbGridTextCellTemplate",
        valueTemplateID: "RawbGridTextCellTemplate",
        footerTemplateID: "RawbGridTextCellTemplate",
        selection: undefined,
        columnSortingFn: function (grid, valueField) {
            var data = (grid ? grid.data() : []);
            var n = data.length;
            if (n <= 1) return;
            var cmpable = function (x) {
                var u = ko.unwrap(x && x[valueField]);
                var v = u && u.valueOf();
                return ( typeof(v) === "number" // This handles dates, too.
                       ? v
                       : (v || "").toString().toLowerCase()
                       );
            };
            var ord = 1;
            var cmp = function (p, q) {
                var pc = cmpable(p);
                var qc = cmpable(q);
                return ord * (pc < qc ? -1 : pc === qc ? 0 : 1);
            };
            if (cmp(data[0], data[n - 1]) < 0) ord = -1;
            data.sort(cmp);
            // If we don't do this, the display won't update!
            grid.data(data);
            grid.topRowIndex(0);
        }
    };

    /*

!! rawb.Grid({arg, ...})
*Template:* _RawbGrid_

An high-performance, highly configurable data grid.  High performance comes
from rendering only what is visible on the grid (the performance is the
same displaying a ten-row grid whether you have a hundred rows or a
hundred thousand rows) and performing minimal re-rendering when the grid is
scrolled.  Configurability largely comes from the ability to
specify individual cell templates.

The grid is divided vertically into three regions: the header, values, and
footer.  The optional header and footer are static and do not move when
the grid values are scrolled vertically.

The grid is also divided horizontally into three regions: the left, middle,
and right.  The optional left and right regions are static and do not move
when the grid values are scrolled horizontally.

|| Left headers || Middle headers || || Right headers ||
| Left values | Middle values | | Right values |
| Left values | Middle values | | Right values |
| Left values | Middle values | | Right values |
|             |               | |              |
|| Left footers || Middle footers || || Right footers ||

Resizing splitters separate each column of cells.

*Parameters*

*data:* _array of objects (not scalars)_
Optional (default _[]_); may be observable; exposed as an observable.

The grid data to be displayed.

*selection:* _observable object._
Optional (default _undefined_); must be observable; exposed as an observable.

The currently selected grid row, if any; undefined if no row is selected.

*width:* _number_
Optional (default _400_); may be observable; exposed as an observable.

The width in pixels of the grid.

*height:* _number_
Optional (default _200_); may be observable; exposed as an observable.

The height in pixels of the grid.

*vScrollBarWidth:* _number_
Optional (default _17_); may be observable; exposed as an observable.

The width in pixels of the vertical scrollbar (when displayed).
The vertical scrollbar is removed if all rows in data are currently
visible in the grid.

*hScrollBarHeight:* _number_
Optional (default _17_); may be observable; exposed as an observable.

The height in pixels of the horizontal scrollbar.

*resizerWidth:* _number_
Optional (default _1_); may be observable; exposed as an observable.

The width in pixels of the resizer dividers.

*headerRowHeight:* _number_
Optional (default _25_); may be observable; exposed as an observable.

The height in pixels of the header row.

*valueRowHeight:* _number_
Optional (default _25_); may be observable; exposed as an observable.

The height in pixels of each value row.

*footerRowHeight:* _number_
Optional (default _25_); may be observable; exposed as an observable.

The height in pixels of the footer row.

*rowsPerStripe:* _number_
Optional (default _1_); may be observable; exposed as an observable.

The number of adjacent rows forming each alternating "zebra stripe".
Set to 0 to suppress zebra striping.

*click:* _function (row)_
Optional (default _undefined_); not exposed.

The action to carry out when the user clicks on a value row.

*dblClick:* _function (row)_
Optional (default _undefined_); not exposed.

The action to carry out when the user double-clicks on a value row.

*enterKey:* _function ()_
Optional (default _undefined_); exposed.

The action to carry out when the user hits the enter key while the grid has focus.

*escKey:* _function ()_
Optional (default _undefined_); exposed.

The action to carry out when the user hits the escape key while the grid has focus.

*includeResizingHandle:* _boolean_
Optional (default _undefined_); exposed.

If true, the grid is displayed with a resizing handle in its lower right corner.
If false, the grid does not display a resizing handle.

*leftColumns:* _array of column specs_
Optional (default _[]_); must not be an observable; not exposed.

The specification for the fixed columns in the left part of the grid.  See [Column specs].

*rightColumns:* _array of column specs_
Optional (default _[]_); must not be an observable; not exposed.

The specification for the fixed columns in the right part of the grid.  See [Column specs].

*columns:* _array of column specs_
Optional (default _[]_); must not be an observable; not exposed.

The specification for the columns in the middle part of the grid to which the
horizontal scrollbar applies.  See [Column specs].

*Grid properties and methods:*

*topRowIndex:* _observable number_
The index into _data_ of the top-most row currently displayed in the grid.

*scroll(delta):* _function (number)_
Calling _scroll(n)_ scrolls the displayed grid rows up by _n_ rows (or down if _n_ is negative). 

*numRows:* _computed number_
The number of value row lines displayed in the grid (rounded up if the last displayed row is partially
obscured due to the grid _height_).

*dispose():* _function ()_
This function clears up all subscriptions etc. created by the grid.
Use this when the _Grid_ instance goes out of scope, if you are concerned about potential memory leaks.

*Column specs*

Each column spec. is an object specifying the following properties:

    *width:* _number_ or _string_
    Optional (default _10_); must not be an observable.

    The initial width in pixels of the column.  (If specified as a string of the form "nn%"
    then this is computed as that percentage of the remaining pixel width after
    the fixed width columns are taken into account.)

    *valueField:* _string_
    Required; must not be an observable.

    The name of the field in the data row to display in this column.

    *valueFormatter:* _function (any) -> any_
    Optional (default _identity_); must not be an observable.

    An optional formatting function to be applied to the row field in the
    cell template.

    *valueTemplateID:* _string_
    Optional (default _"RawbGridTextCellTemplate"_)

    The ID of the HTML element holding the template for rendering value cells in this
    column.

    *header:* _any_
    Optional (default _undefined_); must not be an observable.

    The header value to display in this column.

    *headerFormatter:* _function (any) -> any_
    Optional (default _identity_); must not be an observable.

    An optional formatting function to be applied to the header value in the
    cell template.

    *headerTemplateID:* _string_
    Optional (default _"RawbGridTextCellTemplate"_)

    The ID of the HTML element holding the template for rendering the header cell
    in this column.

    *headerClick:* _function ()_
    Optional (default _is the column sorting function_); must not be an observable.

    The action to carry out when the user clicks on the header cell
    in this column.  The default column sorting function works as follows: if the first
    and last value cells in this column are in ascending order, then the sort is to
    descending order, otherwise it is to ascending order.  Comparison works as expected
    on numbers, booleans, dates, and strings (string comparison is case and culture
    insensitive).

    *headerDblClick:* _function ()_
    Optional (default _undefined_); must not be an observable.

    The action to carry out when the user double-clicks on the header cell
    in this column.

    *footer:* _any_
    Optional (default _undefined_); must not be an observable.

    The footer value to display in this column.

    *footerFormatter:* _function (any) -> any_
    Optional (default _identity_); must not be an observable.

    An optional formatting function to be applied to the footer value in the
    cell template.

    *footerTemplateID:* _string_
    Optional (default _"RawbGridTextCellTemplate"_)

    The ID of the HTML element holding the template for rendering the footer cell
    in this column.

    *footerClick:* _function ()_
    Optional (default _undefined_); must not be an observable.

    The action to carry out when the user clicks on the footer cell
    in this column.

    *footerDblClick:* _function ()_
    Optional (default _undefined_); must not be an observable.

    The action to carry out when the user double-clicks on the footer cell
    in this column.


    */
    rawb.Grid = function (args) {

        var self = this;

        var getArg = function (optName) {
            var x = (args || {})[optName];
            return ko.isObservable(x) ? x : ko.observable(x !== undefined ? x : rawb.GridDefaults[optName]);
        };

        self.data = getArg("data");
        self.selection = getArg("selection");
        self.width = getArg("width");
        self.height = getArg("height");
        self.visibleVScrollBarWidth = getArg("vScrollBarWidth");
        self.hScrollBarHeight = getArg("hScrollBarHeight");
        self.resizerWidth = getArg("resizerWidth");
        self.headerRowHeight = getArg("headerRowHeight");
        self.valueRowHeight = getArg("valueRowHeight");
        self.footerRowHeight = getArg("footerRowHeight");
        self.rowsPerStripe = getArg("rowsPerStripe");
        self.valueRowHeight.subscribe(function (x) {
            if (!isFinite(x) || x <= 0) {
                alert("rawb.Grid: valueRowHeight must be a positive number.  Things are about to go wrong...");
            }
        });
        var clk = args.click;
        var dblClk = args.dblClick;
        self.valueRowClick = function (x) { if (clk) clk(x); return true; };
        self.valueRowDblClick = function (x) { if (dblClk) dblClk(x); return true; };
        self.enterKey = args.enterKey;
        self.escKey = args.escKey;
        self.includeResizingHandle = args.includeResizingHandle;

        var identityFn = function (x) { return x; };

        var widthLimitFn = function (x) { return Math.max(1, x); }

        var hConsistencyPred = function () { return 0 < self.middleWidth(); }

        var processedCols = function (columns, left) {
            var cols = [];
            columns.slice().forEach(function (x) {
                var width = ko.observable(x.width || 10);
                var resizer = new rawb.HDragger(width, widthLimitFn, hConsistencyPred);
                var headerTemplateID = x.headerTemplateID || rawb.GridDefaults.headerTemplateID;
                var valueTemplateID = x.valueTemplateID || rawb.GridDefaults.valueTemplateID;
                var footerTemplateID = x.footerTemplateID || rawb.GridDefaults.footerTemplateID;
                var headerFormatter = x.headerFormatter || identityFn;
                var valueFormatter = x.valueFormatter || identityFn;
                var footerFormatter = x.footerFormatter || identityFn;
                var header = x.header;
                var valueField = x.valueField;
                var footer = x.footer;
                var headerClick = x.headerClick || function () { rawb.GridDefaults.columnSortingFn(self, valueField); };
                var headerDblClick = x.headerDblClick;
                var footerClick = x.footerClick;
                var footerDblClick = x.footerDblClick;
                var col = {
                    left: left,
                    width: width,
                    resizer: resizer,
                    headerTemplateID: headerTemplateID,
                    valueTemplateID: valueTemplateID,
                    footerTemplateID: footerTemplateID,
                    headerFormatter: headerFormatter,
                    valueFormatter: valueFormatter,
                    footerFormatter: footerFormatter,
                    headerClick: headerClick,
                    headerDblClick: headerDblClick,
                    footerClick: footerClick,
                    footerDblClick: footerDblClick,
                    header: header,
                    valueField: valueField,
                    footer: footer
                };
                cols.push(col);
                left = ko.computed(function () { return col.left() + width() + self.resizerWidth(); });
            });

            return cols;
        };

        self.leftOffset = ko.observable(0);
        self.middleOffset = ko.observable(0);
        var negMiddleOffset = ko.computed(function () { return -self.middleOffset(); });
        self.rightOffset = ko.observable(0);
        var leftColumns = args.leftColumns || [];
        var middleColumns = args.columns || [];
        var rightColumns = args.rightColumns || [];

        // Fix up the percentage column widths.
        var totalPxColWidths = self.visibleVScrollBarWidth() +
            self.resizerWidth() * (leftColumns.length + middleColumns.length + rightColumns.length);
        var prepColWidth1 = function (x) {
            if (typeof(x.width) === "string" && x.width[x.width.length - 1] !== "%") x.width = +x.width;
            if (typeof(x.width) === "number") totalPxColWidths += x.width;
        };
        leftColumns.forEach(prepColWidth1);
        middleColumns.forEach(prepColWidth1);
        rightColumns.forEach(prepColWidth1);
        var onePcInPx = (this.width() - totalPxColWidths) / 100;
        var prepColWidth2 = function (x) {
            if (typeof (x.width) === "string") x.width = Math.max(1, Math.floor(parseFloat(x.width) * onePcInPx));
        }
        leftColumns.forEach(prepColWidth2);
        middleColumns.forEach(prepColWidth2);
        rightColumns.forEach(prepColWidth2);

        self.leftCols = processedCols(leftColumns, self.leftOffset);
        self.middleCols = processedCols(middleColumns, negMiddleOffset);
        self.rightCols = processedCols(rightColumns, self.rightOffset);

        self.valuesHeight = ko.computed(function () {
            return self.height() -
                self.headerRowHeight() -
                self.hScrollBarHeight() -
                self.footerRowHeight();
        });
        self.numRows = ko.computed(function () {
            return Math.ceil(self.valuesHeight() / self.valueRowHeight());
        });
        self.topRowIndex = ko.observable(0);
        // Hide the vertical scrollbar if it isn't needed.
        self.vScrollBarWidth = ko.computed(function () {
            var needVScrollBar =
                (self.valuesHeight() < (self.data() || []).length * self.valueRowHeight()) ||
                (self.data().length < self.topRowIndex() + self.numRows());
            return self.visibleVScrollBarWidth() * needVScrollBar;
        });

        var totalColWidth = function (cols) {
            return cols.reduce(function (a, x) { return a + x.width() + self.resizerWidth(); }, 0);
        };

        self.leftWidth = ko.computed(function () { return totalColWidth(self.leftCols); });
        self.rightWidth = ko.computed(function () { return totalColWidth(self.rightCols); });
        self.totalMiddleWidth = ko.computed(function () { return totalColWidth(self.middleCols); });
        self.middleWidth = ko.computed(function () {
            return self.width() -
                self.leftWidth() -
                self.rightWidth() -
                self.vScrollBarWidth();
        });
        self.leftLeft = ko.computed(function () { return 0; });
        self.middleLeft = ko.computed(function () { return self.leftLeft() + self.leftWidth(); });
        self.vScrollBarLeft = ko.computed(function () { return self.middleLeft() + self.middleWidth(); });
        self.rightLeft = ko.computed(function () { return self.vScrollBarLeft() + self.vScrollBarWidth(); });
        self.headerTop = ko.computed(function () {
            return 0;
        });
        self.valuesTop = ko.computed(function () {
            return self.headerTop() + self.headerRowHeight();
        });
        self.hScrollBarTop = ko.computed(function () {
            return self.valuesTop() + self.valuesHeight();
        });
        self.footerTop = ko.computed(function () {
            return self.hScrollBarTop();
        });
        self.overflowX = ko.computed(function () {
            return ( self.hScrollBarHeight() ? "scroll" : "hidden" );
        });
        // Update the displayed rows when the top row index changes.
        self.updateRows = function (rows) {
            if (rows === undefined) rows = self.rows();
            var topRowIdx = self.topRowIndex.peek();
            for (var i = 0; i < rows.length ; i++) {
                var row = rows[i];
                var src = self.data.peek()[topRowIdx + i] || {};
                row.__rawbRowSrc = src;
                for (var j = 0; j < self.leftCols.length; j++) {
                    row[self.leftCols[j].valueField](src[self.leftCols[j].valueField]);
                }
                for (var j = 0; j < self.middleCols.length; j++) {
                    row[self.middleCols[j].valueField](src[self.middleCols[j].valueField]);
                }
                for (var j = 0; j < self.rightCols.length; j++) {
                    row[self.rightCols[j].valueField](src[self.rightCols[j].valueField]);
                }
            }
        };
        // This is the array of rows currently being displayed.
        // Each element is a distinct observable to reduce the amount of
        // redrawing needed when scrolling the grid.
        self.rows = ko.computed(function () {
            var rows = [];
            var numRows = Math.min(self.numRows(), self.data().length);
            for (var i = 0; i < numRows; i++) {
                var row = {};
                for (var j = 0; j < self.leftCols.length; j++) {
                    row[self.leftCols[j].valueField] = ko.observable();
                }
                for (var j = 0; j < self.middleCols.length; j++) {
                    row[self.middleCols[j].valueField] = ko.observable();
                }
                for (var j = 0; j < self.rightCols.length; j++) {
                    row[self.rightCols[j].valueField] = ko.observable();
                }
                rows.push(row);
            }
            self.updateRows(rows);
            return rows;
        });
        var dataLength = ko.computed(function () { return (self.data() || []).length; });
        self.vScrollBarContentHeight = ko.computed(function () {
            return self.valueRowHeight() * dataLength();
        });
        self.vScrollBarScrollTop = ko.observable(self.valueRowHeight() * self.topRowIndex());
        var topRowIndexSubscription = self.topRowIndex.subscribe(function (x) {
            self.vScrollBarScrollTop(self.valueRowHeight() * x);
            self.updateRows();
        });
        var scrollTopSubscription = self.vScrollBarScrollTop.subscribe(function (x) {
            self.topRowIndex(Math.round(x / (self.valueRowHeight() || 1)));
        });
        self.pendingScrollDelta = 0;
        self.scroll = function (scrollDelta) {
            // This mechanism ensures we don't end up with a backlog of scroll events.
            var needScrollTask = self.pendingScrollDelta === 0;
            self.pendingScrollDelta += scrollDelta;
            // Cap updates at 50 per second.
            if (!self.handlePendingScrollTask) self.handlePendingScrollTask = setTimeout(self.handlePendingScroll, 20);
        };
        self.handlePendingScroll = function () {
            self.topRowIndex(
                Math.max(0, // Lower bound.
                Math.min(self.data().length - 1, // Upper bound.
                    self.topRowIndex() + self.pendingScrollDelta
                ))
            );
            self.pendingScrollDelta = 0;
            self.handlePendingScrollTask = undefined;
        };
        self.keyDown = function (data, evt) {
            var delta = 0;
            switch (evt.keyCode) {
                case 13: if (self.enterKey) self.enterKey(); return true;
                case 27: if (self.escKey) self.escKey(); return true;
                case 38: delta = -1; break;                                 // Up arrow.
                case 40: delta = 1; break;                                  // Down arrow.
                case 33: delta = -Math.max(1, self.numRows() - 1); break;   // PgUp.
                case 34: delta = Math.max(1, self.numRows() - 1); break;    // PgDown.
                case 36: delta = -Infinity; break;                          // Home.
                case 35: delta = Infinity; break;                           // End.
            }
            self.scroll(delta);
            return true; // Allow the event to propagate.
        };
        self.mouseWheel = function (data, evt) {
            var delta = (0 < evt.wheelDelta ? -1 : 1) * Math.max(1, self.numRows() - 1);
            self.scroll(delta);
        };

        self.leftViewModel = {
            left: self.leftLeft,
            width: self.leftWidth,
            headerTop: self.headerTop,
            valuesTop: self.valuesTop,
            footerTop: self.footerTop,
            height: self.height,
            headerRowHeight: self.headerRowHeight,
            valuesHeight: self.valuesHeight,
            footerRowHeight: self.footerRowHeight,
            sideCssClass: "rawb-grid-left",
            headerViewModel: {
                headerTop: self.headerTop,
                headerRowHeight: self.headerRowHeight,
                resizerWidth: self.resizerWidth,
                columns: self.leftCols
            },
            valuesViewModel: {
                valuesTop: self.valuesTop,
                valueRowHeight: self.valueRowHeight,
                resizerWidth: self.resizerWidth,
                rows: self.rows,
                columns: self.leftCols,
                valueRowClick: self.valueRowClick,
                valueRowDblClick: self.valueRowDblClick,
                rowsPerStripe: self.rowsPerStripe,
                topRowIndex: self.topRowIndex,
                data: self.data,
                selection: self.selection
            },
            footerViewModel: {
                footerTop: self.footerTop,
                footerRowHeight: self.footerRowHeight,
                resizerWidth: self.resizerWidth,
                columns: self.leftCols
            }
        };

        self.middleViewModel = {
            left: ko.observable(0),
            width: self.totalMiddleWidth,
            top: ko.observable(0),
            headerTop: self.headerTop,
            valuesTop: self.valuesTop,
            footerTop: self.footerTop,
            height: self.height,
            headerRowHeight: self.headerRowHeight,
            valuesHeight: self.valuesHeight,
            footerRowHeight: self.footerRowHeight,
            sideCssClass: "rawb-grid-middle",
            overflowX: self.overflowX,
            headerViewModel: {
                headerRowHeight: self.headerRowHeight,
                resizerWidth: self.resizerWidth,
                columns: self.middleCols
            },
            valuesViewModel: {
                valueRowHeight: self.valueRowHeight,
                resizerWidth: self.resizerWidth,
                rows: self.rows,
                columns: self.middleCols,
                valueRowClick: self.valueRowClick,
                valueRowDblClick: self.valueRowDblClick,
                rowsPerStripe: self.rowsPerStripe,
                topRowIndex: self.topRowIndex,
                data: self.data,
                selection: self.selection
            },
            footerViewModel: {
                footerRowHeight: self.footerRowHeight,
                resizerWidth: self.resizerWidth,
                columns: self.middleCols
            }
        };

        self.rightViewModel = {
            left: self.rightLeft,
            width: self.rightWidth,
            top: self.headerTop,
            headerTop: self.headerTop,
            valuesTop: self.valuesTop,
            footerTop: self.footerTop,
            height: self.height,
            headerRowHeight: self.headerRowHeight,
            valuesHeight: self.valuesHeight,
            footerRowHeight: self.footerRowHeight,
            sideCssClass: "rawb-grid-right",
            headerViewModel: {
                headerRowHeight: self.headerRowHeight,
                resizerWidth: self.resizerWidth,
                columns: self.rightCols
            },
            valuesViewModel: {
                valueRowHeight: self.valueRowHeight,
                resizerWidth: self.resizerWidth,
                rows: self.rows,
                columns: self.rightCols,
                valueRowClick: self.valueRowClick,
                valueRowDblClick: self.valueRowDblClick,
                rowsPerStripe: self.rowsPerStripe,
                topRowIndex: self.topRowIndex,
                data: self.data,
                selection: self.selection
            },
            footerViewModel: {
                footerRowHeight: self.footerRowHeight,
                resizerWidth: self.resizerWidth,
                columns: self.rightCols
            }
        };

        self.nativeVScrollBarViewModel = new rawb.NativeVScrollBar({
            height: self.valuesHeight,
            width: self.vScrollBarWidth,
            left: self.vScrollBarLeft,
            top: self.valuesTop,
            scrollTop: self.vScrollBarScrollTop,
            contentHeight: self.vScrollBarContentHeight
        });

        self.hScrollBarViewModel = {
            left: self.middleLeft,
            width: self.middleWidth,
            top: self.hScrollBarTop,
            height: self.hScrollBarHeight,
            scrollBar: self.hScrollBar
        };

        if (self.includeResizingHandle) {
            var wLimit = self.leftWidth() +
                self.vScrollBarWidth() +
                self.rightWidth();
            var wLimitFn = function (x) { return Math.max(wLimit, x); };
            var hLimit = self.headerRowHeight() +
                self.valueRowHeight() +
                self.hScrollBarHeight() +
                self.footerRowHeight();
            var hLimitFn = function (x) { return Math.max(hLimit, x); };
            self.resizingDragger = new rawb.Dragger(self.width, wLimitFn, null, self.height, hLimitFn, null, 100);
        } else {
            self.resizingDragger = undefined;
        }

        self.subscriptions = [
            self.leftWidth,
            self.rightWidth,
            self.totalMiddleWidth,
            self.middleWidth,
            self.leftLeft,
            self.middleLeft,
            self.vScrollBarLeft,
            self.rightLeft,
            self.valuesHeight,
            self.headerTop,
            self.valuesTop,
            self.hScrollBarTop,
            self.footerTop,
            self.numRows,
            self.rows,
            dataLength,
            self.vScrollBarContentHeight,
            topRowIndexSubscription,
            scrollTopSubscription,
            self.data.subscribe(function () { self.selection(undefined); })
        ];

        self.dispose = function () {
            self.subscriptions.forEach(function (x) { x.dispose(); });
            self.subscriptions = [];
        };

        return self;
    };

    /*

!! rawb.DropDown({arg, ...})

The DropDown is a more versatile version of the HTML 'select' control,
supporting autocompletion, on-demand and asynchronous data fetching,
and interactive filtering and sorting.

*Example HTML:*
{code:html}
<!-- ko template: { name: 'RawbDropDownTemplate', data: dd } -->
<!-- /ko -->
{code:html}

*Example JavaScript:*
{code:javascript}
var options = ko.observable([{a: "Alice", b: 1}, {a: "Bob", b: 2}, {a: "Cissy", b: 3}]);
var dd = new rawb.DropDown({options: options, fieldName: "a"});
ko.applyBindings({dd: dd});
{code:javascript}

*Invariant*

If the _input_ exactly matches a filtered _field_ in _options_, then _selection_ is set
to the first such field.  Otherwise _selection_ is set to _undefined_.

*Parameters*

*input*: _observable string_
Optional (default _""_); exposed as observable.

The text displayed in the input box, reflecting either the
current _selection_ or what the user has typed in.

*enabled*: _boolean_
Optional (default _true_); may be observable; exposed as observable.

If _true_, the input field is enabled.
If _false_, the input field is disabled.

*options*: _array of object_
Optional (default _[]_); exposed as observable.

The array of option objects from which a selection may be made
(note that this _must_ be an array of objects, it cannot be an
array of scalars, such as numbers, strings, dates, etc.).

*fieldName:* _string_
Required; must not be an observable; exposed as non-observable.

The name of the field in the _options_ objects against which
_input_ is matched to decide the value of _selection_.

*selection:* _observable object_
Optional (default _undefined_); must be writable observable; exposed as observable.
The first value _x_ drawn from _options_ such that _x[fieldName]_ exactly
matches _input_ (_selection_ is _undefined_ if there is no such match).

*matchFn:* _function (DropDown) -> array of Match_
Optional (default is built-in).

The function used to filter and order _options_ partially matching
_input_, returning a list of _Match_ objects (see below).

The default _matchFn_ deems the _input_ to match all _options_ where the _input_ is a
(case insensitive) substring of the option _fieldName_ property.

The default match comparison function used for ordering is _matchComparisonFn_ (see below).

The _DropDown_ control invokes _matchFn_ whenever the _input_ or _options_ are updated.
The _matchFn_ must (a) filter out _options_ which do not match the _input_, (b) order the
_options_ which do match the _input_, and (c) convert those matching _options_ into _Match_
objects (see below).

*Match objects:* _{ option: object, all: string, match: { prefix: string, match: string, suffix: string } }_
The default _matchComparisonFn_ and _matchTemplateID_ HTML template assume each _Match_
record will include the fields listed:
_option_ is the matching item from _options_;
_all_ is the complete _option[fieldName]_ value;
_prefix_ is the part of the _fieldName_ value preceding the match against _input_;
_match_ is the part of the _fieldName_ value matching the _input_ (the case may be different);
_suffix_ is the part of the _fieldName_ value following the match against _input_.

*matchComparisonFn:* _function (Match, Match) -> number_
Optional (default is built-in).

The default match comparison function orders as follows: matches with empty prefixes
preceed those with non-empty prefixes; within each partition, matches are alphabetically sorted
in case insensitive fashion.

*fetchFn:* _function (DropDown)_
Optional (default does nothing).

The function used to update _options_ in response to the _input_ changing.
This allows _options_ to be fetched, e.g., via an AJAX call.

*matchTemplateID:* _string_
Optional (default _"RawbDropDownMatchTemplate"_).

The HTML template used to display matches in the drop-down grid.

*width:* _number_
Optional (default _200_); may be observable; exposed as observable.

The width in pixels of the input field and drop-down grid.

*inputHeight:* _number_
Optional (default _20_); may be observable; exposed as observable.

The height in pixels of the input field.

*gridHeight:* _number_
Optional (default _200_); may be observable; exposed as observable.

The maximum height in pixels of the drop-down grid.

*gridRowHeight:* _number_
Optional (default _24_); may be observable; exposed as observable.

The maximum height in pixels of each match row in the drop-down grid.

*gridFieldWidth:* _number_
Optional (default _1000_); may be observable; exposed as observable.

The scrollable width in pixels of each match row in the drop-down grid (only
needed if the _hScrollBarHeight_ has been set).

*gridVScrollBarWidth:* _number_
Optional (default _10_); may be observable; exposed as observable.

The width in pixels of the vertical scrollbar on the drop-down grid (only displayed
when there are more matches than can be displayed in the given _gridHeight_).

*gridHScrollBarHeight:* _number_
Optional (default _0_); may be observable; exposed as observable.

The height in pixels of the horizontal scrollbar (setting this to zero means
no horizontal scrollbar will be displayed).

*Methods*

*dispose()*: _function ()_
Disposes Knockout subscriptions created by the _DropDown_.
Use this when the _DropDown_ goes out of scope if you are concerned about memory leaks.

    */
    rawb.DropDownDefaults = {
        width: 200,
        options: [],
        input: "",
        enabled: true,
        selection: undefined,
        fetchFn: function (dd) {
            // Do nothing by default.
        },
        matchComparisonFn: function (x, y) {
            var xp = x.match.prefix === "";
            var xa = (x.all || "").toLowerCase();
            var yp = y.match.prefix === "";
            var ya = (y.all || "").toLowerCase();
            return ( xp && !yp
                   ? -1
                   : !xp && yp
                   ?  1
                   : (ya < xa) - (xa < ya)
                   );
        },
        matchFn: function (dd) {
            var opts = dd.options() || [];
            var inpt = dd.input() || "";
            var fld  = dd.fieldName;
            var re   = new RegExp(inpt.replace(/[^\w ]/g, "\\$&"), "i");
            var cmp = dd.matchComparisonFn;
            var mchs = opts.map(function (x) { return { option: x, reMatch: re.exec(x[fld]) }; })
                           .filter(function (x) { return x.reMatch; })
                           .map(function (x) {
                               var i = x.reMatch.index;
                               var l = x.reMatch[0].length;
                               var s = x.option[fld];
                               x.all = s;
                               x.match = {
                                   prefix: s.substr(0, i),
                                   match:  s.substr(i, l),
                                   suffix: s.substr(i + l)
                               };
                               return x;
                           });
            mchs.sort(cmp);
            return mchs;
        },
        inputHeight: 20,
        gridHeight: 200,
        gridRowHeight: 24,
        gridFieldWidth: 1000,
        matchTemplateID: "RawbDropDownMatchTemplate",
        gridVScrollBarWidth: rawb.NativeVScrollBarDefaults.width,
        gridHScrollBarHeight: 0,
        gridLeftOffset: 0,
        gridTopOffset: 0
    };

    rawb.DropDown = function (args) {

        var self = this;

        var getArg = function (optName) {
            var x = (args || {})[optName];
            return ko.isObservable(x) ? x : ko.observable(x !== undefined ? x : rawb.DropDownDefaults[optName]);
        };

        self.width = getArg("width");
        self.input = getArg("input");
        self.selection = getArg("selection");
        self.enabled = getArg("enabled");
        self.options = getArg("options");
        self.fieldName = args.fieldName;
        self.fetchFn = args.fetchFn || rawb.DropDownDefaults.fetchFn;
        self.matchComparisonFn = args.matchComparisonFn || rawb.DropDownDefaults.matchComparisonFn;
        self.matchFn = args.matchFn || rawb.DropDownDefaults.matchFn;
        self.matchTemplateID = args.matchTemplateID || rawb.DropDownDefaults.matchTemplateID;
        self.matches = ko.computed(function () {
            return self.matchFn(self);
        });
        self.inputHeight = getArg("inputHeight");
        self.gridFieldWidth = args.gridFieldWidth || rawb.DropDownDefaults.gridFieldWidth;
        self.gridLeft = args.gridLeftOffset || rawb.DropDownDefaults.gridLeftOffset;
        self.gridTop = self.inputHeight() + (args.gridTopOffset || rawb.DropDownDefaults.gridTopOffset);
        self.maxGridHeight = getArg("gridHeight");
        self.gridRowHeight = getArg("gridRowHeight");
        self.gridVScrollBarWidth = getArg("gridVScrollBarWidth");
        self.gridHScrollBarHeight = getArg("gridHScrollBarHeight");
        self.gridHeight = ko.computed(function () {
            return Math.min(
                self.maxGridHeight(),
                (self.matches() ? self.matches().length * self.gridRowHeight() + self.gridHScrollBarHeight() : 0)
            );
        });
        self.displayingGrid = ko.observable(false);
        // The event handlers across the input and the grid are a little bit complicated because
        // (a) Chrome screws you over with respect to calling .select() on an input focus event and
        // (b) there are other ways the user can cancel the input and
        // (c) when the user interacts with the grid, we don't want to lose focus on the input.

        var setHoldOff = function (x) { self.holdOff = x; };

        // When the input becomes focussed, show the contents as selected.
        self.inputFocus = function (data, evt) {
            self.displayingGrid(true);
            if (!evt) return true;
            var target = evt.target;
            if (target && target.select) target.select();
            return true; // Propagate the event.
        };
        // Prevent Chrome from trashing the .select() call in .inputFocus.
        self.inputMouseUp = function (data, evt) {
            return false;
        },
        // We want to hide the options grid when the input loses focus,
        // except when the user is clicking the scrollbar on the options grid!
        self.inputBlur = function (data, evt) {
            setTimeout(function () { self.displayingGrid(self.holdOff); setHoldOff(false); }, 0); // Wait for the grid to respond.
            return true; // Propagate the event.
        };
        // When the user interacts with the grid, we prevent .inputBlur above from closing the grid
        // before the grid receives any events.
        self.gridMouseDown = function (data, evt) {
            setHoldOff(true); // Tell the input blur handler to wait for the grid to respond.
            return true; // Propagate the event.
        };
        self.gridMouseOut = function (data, evt) {
            setTimeout(function () { setHoldOff(false); }, 0); // Wait for the input to respond.
            return true; // Propagate the event.
        };
        // Close the grid if the user made a selection.
        self.gridClick = function (data, evt) {
            self.displayingGrid(self.holdOff);
            return true;
        };
        var chgSelection = function (delta) {
            var i = self.grid.topRowIndex() + (self.grid.selection() ? delta : 0);
            i = Math.max(0, Math.min(self.matches().length - 1, i));
            self.grid.topRowIndex(i);
            self.grid.selection(self.matches()[i]);
        };
        self.inputKeyDown = function (data, evt) {
            if (!evt) return true;
            self.displayingGrid(true);
            var kc = evt.keyCode;
            switch (kc) {
                case 9: // TAB
                case 13: // ENTER
                    if (self.matches().length === 1) self.grid.selection(self.matches()[0]);
                    if (!self.grid.selection()) break;
                    self.selection(self.grid.selection().option);
                    break;
                case 27: self.selection(undefined);                 break; // ESC
                case 38: chgSelection(-1);                          break; // UP ARROW
                case 40: chgSelection(1);                           break; // DOWN ARROW
                case 33: chgSelection(-self.grid.rows().length);    break; // PG UP
                case 34: chgSelection(self.grid.rows().length);     break; // PG DOWN
            }
            return true; // Propagate the event.
        };
        self.inputKeyPress = function (data, evt) {
            if (self.fetchFn) self.fetchFn(self);
            return true; // Propagate the event.
        };
        self.grid = new rawb.Grid({
            data: self.matches,
            width: self.width,
            height: self.gridHeight,
            valueRowHeight: self.gridRowHeight,
            headerRowHeight: 0,
            footerRowHeight: 0,
            columns: [{
                width: self.gridFieldWidth - self.gridVScrollBarWidth(),
                valueField: "match",
                valueTemplateID: self.matchTemplateID
            }],
            resizerWidth: 0,
            vScrollBarWidth: self.gridVScrollBarWidth,
            hScrollBarHeight: self.gridHScrollBarHeight,
            click: function (row) {
                self.displayingGrid(false);
                self.holdOff = false;
                if (!row) return;
                var opt = row.option;
                self.selection(opt);
                self.input(opt[self.fieldName] + "");
            }
        });
        // The relationships between the input, the selection, the matches, and the grid.
        // And anything else we need to clean up on calling dispose.
        self.subscriptions = [
            self.matches,
            self.selection.subscribe(function (x) {
                if (!x) return;
                var newInput = x[self.fieldName];
                if (self.input() !== newInput) self.input(newInput);
            }),
            self.matches.subscribe(function (x) {
                var mchs = self.matches();
                var inp = self.input();
                var sln = (mchs.length === 1 && mchs[0].all === inp) ? mchs[0].option : undefined;
                if (self.selection() !== sln) self.selection(sln);
                self.grid.topRowIndex(0);
            })
        ];
        self.dispose = function () {
            self.grid.dispose();
            self.subscriptions.forEach(function (x) { x.dispose(); });
            self.subscriptions = [];
        };
        return self;
    };

    /*

!! rawb.ManagedInput({arg, ...})
*Templates:* _RawbManagedInputTemplate_

The ManagedInput is useful for input fields which show "formatted" data except when being edited
(e.g., the "formatted" contents might show a currency symbol followed by a number with thousands
separators, while the "unformatted" contents while editing are simply an undecorated number).

*Example HTML:*
{code:html}
<!-- ko template: { name: 'RawbManagedInputTemplate', data: mi } -->
<!-- /ko -->
{code:html}

*Example JavaScript:*
{code:javascript}
var value = ko.observable();
var displayFormatter = function (value) {
    if (!isFinite(value)) return "";
    var s = value.toFixed(2);
    for (var i = s.indexOf(".") - 3; 0 < i; i -= 3)
        s = s.substr(0, i) + "," + s.substr(i);
    return "$ " + s;
};
var editFormatter = function (value) {
    return (isFinite(value) ? value.toFixed(2) : "");
},
var parser = function (input) {
    return (input != "" ? +input : undefined);
}
var mi = new rawb.ManagedInput({
    value: value,
    displayFormatter: displayFormatter,
    editFormatter: editFormatter,
    parser: parser
});
ko.applyBindings({mi: mi});
{code:javascript}

*Parameters*

*input:* _observable string_
Optional (default _""_); exposed as observable.

The "managed" value in the input box.

*value:* _observable any_
Optional (default _undefined_); exposed as observable.

The underlying "unmanaged" observable.

*enabled:* _observable boolean_
Optional (default _true_); exposed as observable.

If _true_, the input field is enabled.
If _false_, the input field is disabled.

*displayFormatter:* _function (value) -> string_
Optional (default _toString_); exposed.

This function runs when the _value_ changes and is used to format that value to update the _input_.
This function should never throw an exception -- in particular, it should handle falsy values.
(E.g.: formatting numbers to include thousands separators.)

*editFormatter:* _function (value) -> string_
Optional (default _toString_); exposed.

This function runs when the input box becomes focussed and is used to format _value_ to update the _input_.
This function should never throw an exception -- in particular, it should handle falsy values.
(E.g.: formatting numbers to not include thousands separators.)

*parser:* _function (string) -> any_
Optional (default _the identity function_); exposed.

This function runs when the _input_ changes and is used to update the _value_.
This function should never throw an exception -- in particular, it should handle falsy values.
(E.g.: formatting numbers to not include thousands separators.)

*Methods*

*dispose():* _function ()_

This function clears up all subscriptions etc. created by the _ManagedInput_ instance.
Use this when the _ManagedInput_ instance goes out of scope, if you are concerned about potential memory leaks.

    */

    rawb.ManagedInputDefaults = {
        displayFormatter: function (x) { return (x == undefined || x == null) ? "" : x.toString(); },
        editFormatter: function (x) { return (x == undefined || x == null) ? "" : x.toString(); },
        parser: function (x) { return x; },
        input: "",
        enabled: true,
        value: undefined
    };

    rawb.ManagedInput = function (args) {

        var self = this;

        var getArg = function (optName) {
            var x = (args || {})[optName];
            return ko.isObservable(x) ? x : ko.observable(x !== undefined ? x : rawb.ManagedInputDefaults[optName]);
        };

        self.displayFormatter = args.displayFormatter || rawb.ManagedInputDefaults.displayFormatter;
        self.editFormatter = args.editFormatter || rawb.ManagedInputDefaults.editFormatter;
        self.parser = args.parser || rawb.ManagedInputDefaults.parser;
        self.input = getArg("input");
        self.enabled = getArg("enabled");
        self.value = getArg("value");

        self.locked = false; // Used to prevent cyclic updates in subscriptions.
        self.focussed = false;
        self.inputFocus = function (data, evt) {
            if (self.locked || self.focussed) return true;
            self.focussed = true; // Prevent overlapping calls.
            self.locked = true;
            var x = self.editFormatter(self.value());
            if (self.input() !== x) self.input(x);
            self.locked = false;
            return true; // Propagate the event.
        };
        self.inputBlur = function (data, evt) {
            if (self.locked || !self.focussed) return true;
            self.focussed = false;
            self.locked = true;
            var x = self.parser(self.input());
            self.value(x);
            self.input(self.displayFormatter(x));
            self.locked = false;
            return true;
        };

        self.subscriptions = [
            self.value.subscribe(function (x) {
                if (self.locked) return;
                self.input(self.displayFormatter(x));
            })
        ];

        self.dispose = function () { };

        return self;
    };

})();
